﻿namespace Bowlus.AppHost
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Security.AccessControl;
    using System.Threading;
    using Bowlus.Common;
    using Bowlus.Common.DeploymentActions;

    using Microsoft.Web.Administration;
    using Microsoft.WindowsAzure.ServiceRuntime;

    public interface IAppHostManager
    {
        /// <summary>
        /// Initializes the deployment manager
        /// </summary>
        void Initialize();

        void Run();
    }

    public class AppHostManager : IAppHostManager
    {
        private static readonly object LockObject = new object();

        private readonly List<IISAppInfo> installedApplications = new List<IISAppInfo>();

        private readonly IAppRepository appRepository;

        private string sitesPath;

        private string appCachePath;

        private bool initialized;

        private IPEndPoint endPoint;

        public AppHostManager(IAppRepository repository)
        {
            this.appRepository = repository;
            this.initialized = false;
        }

        /// <summary>
        /// Initializes the deployment manager
        /// </summary>
        public void Initialize()
        {
            try
            {
				if (RoleEnvironment.IsEmulated)
				{
					int id =
						int.Parse(RoleEnvironment.CurrentRoleInstance.Id.Substring(RoleEnvironment.CurrentRoleInstance.Id.Length - 1));
					Thread.Sleep(id*20000);
				}

            	// Initialize Local Resources
                this.sitesPath = RoleEnvironment.GetLocalResource("Sites").RootPath.TrimEnd('\\');
                this.appCachePath = RoleEnvironment.GetLocalResource("AppCache").RootPath.TrimEnd('\\');

                // Configure Security on our directories
                this.ConfigureDirectorySecurity(this.sitesPath);
                this.ConfigureDirectorySecurity(this.appCachePath);
            }
            catch (Exception ex)
            {
                BowlusLogger.LogError(ex.Message);
                throw;
            }

            this.initialized = true;

            this.endPoint = RoleEnvironment.CurrentRoleInstance.InstanceEndpoints["Http"].IPEndpoint;
        }

        public void Run()
        {
            var myActions =
                TenantDeploymentsStore.Instance.TenantDeployment.Where(
                    tenantDeployment =>
                    tenantDeployment.AppHostInstanceId.Equals(RoleEnvironment.CurrentRoleInstance.Id)
                    && tenantDeployment.Status.Equals(TenantDeploymentStatus.DeployRequestSubmitted)).ToList();

            foreach (var tenantDeployment in myActions)
            {
                switch (tenantDeployment.DeploymentAction)
                {
                    case DeploymentActionType.DeployToAppHost:
                        if (this.DeployApplication(tenantDeployment))
                        {
                            tenantDeployment.DeploymentAction = DeploymentActionType.Reroute;
                        }

                        TenantDeploymentsStore.UpdateDeploymentStore(tenantDeployment);
                        break;
                    case DeploymentActionType.DeleteFromAppHost:
                        if (this.DeleteApplication(tenantDeployment))
                        {
                            tenantDeployment.DeploymentAction = DeploymentActionType.Remove;
                        }

                        TenantDeploymentsStore.UpdateDeploymentStore(tenantDeployment);
                        break;
					case DeploymentActionType.MoveFromAppHost:
						if (this.DeleteApplication(tenantDeployment))
						{
							tenantDeployment.DeploymentAction = DeploymentActionType.Move;
						}

						TenantDeploymentsStore.UpdateDeploymentStore(tenantDeployment);
						break;
					default:
                        throw new ArgumentOutOfRangeException();
                }
            }
        }

        private static ApplicationPool GetAppPool(ServerManager sm, string siteName)
        {
            var appPool = sm.ApplicationPools.SingleOrDefault(p => p.Name == siteName);
            if (appPool == null)
            {
                appPool = sm.ApplicationPools.Add(siteName);
                appPool.ManagedRuntimeVersion = "v4.0";
                appPool.ProcessModel.LoadUserProfile = false;
            }

            return appPool;
        }

        private bool RemoveIISApp(IISAppInfo app)
        {
            var serverManager = new ServerManager();
            var siteName = RoleEnvironment.CurrentRoleInstance.Id + "_" + app.ApplicationHostName;
            var site = serverManager.Sites.SingleOrDefault(s => s.Name.Equals(siteName));

            if (null != site)
            {
                // Delete the site and the appPool
                var appPoolName = site.Applications[0].ApplicationPoolName;
                var appPool = serverManager.ApplicationPools.SingleOrDefault(ap => ap.Name == appPoolName);
                serverManager.ApplicationPools.Remove(appPool);
                serverManager.Sites.Remove(site);
                serverManager.CommitChanges();               
            }
            else
            {
                //throw exception    
            }

            // Delete folders we don't use any more
            foreach (var directory in Directory.EnumerateDirectories(this.sitesPath))
            {
                var host = directory.Replace(this.sitesPath, string.Empty).Replace("\\", string.Empty);
                if (this.installedApplications.Count(e => e.ApplicationHostName == host) == 0)
                {
                    Directory.Delete(directory, true);
                }
            }

            // finally, update the local store
            this.installedApplications.Remove(app);

            return true;
        }

        private bool AddIISApp(IISAppInfo app)
        {
            lock (LockObject)
            {
                try
                {
                    var serverManager = new ServerManager();

                    var siteName = RoleEnvironment.CurrentRoleInstance.Id + "_" + app.ApplicationHostName;
                    var sitePath = Path.Combine(app.CurrentPath, "Site");

                    if (serverManager.Sites.Any(s => s.Name.Equals(siteName)))
                    {
                        BowlusLogger.LogError(
                            string.Format(
                                "Site {0} ALREADY EXISTS on server {1}",
                                siteName,
                                RoleEnvironment.CurrentRoleInstance.Id));

                        return true;
                    }

                    // default binding is just the site name (example.org)
                    var newSite = serverManager.Sites.Add(
                        siteName,
                        "http",
                        this.endPoint + ":" + app.ApplicationHostName,
                        sitePath);

                    var hostHeaderParts = app.ApplicationHostName.Split('.');
                    var appPoolName = hostHeaderParts.Length == 3 ? hostHeaderParts[0] : app.ApplicationHostName;

                    newSite.Applications.First().ApplicationPoolName = GetAppPool(serverManager, appPoolName).Name;

                    serverManager.CommitChanges();

                    return true;
                }
                catch (Exception exception)
                {
                    BowlusLogger.LogError(string.Format("Cannot create site for app {0} on server {1}. Details are {2}",
                        app.ApplicationHostName,
                        RoleEnvironment.CurrentRoleInstance.Id,
                        exception));

                    return false;
                }
            }
        }

        private bool DeployApplication(TenantDeployment tenantDeployment)
        {
            if (!this.initialized)
            {
                throw new ApplicationException("AppHost is not initialized yet.");
            }

            BowlusLogger.LogInfo(
                string.Format(
                    "Deploying application {0} for tenant {1} on server {2}",
                    tenantDeployment.Application,
                    tenantDeployment.TenantId,
                    RoleEnvironment.CurrentRoleInstance.Id));

            if (this.installedApplications.Any(app => app.ApplicationHostName.Equals(tenantDeployment.HostName)))
            {
                BowlusLogger.LogError(
                    string.Format(
                        "Application {0} for tenant {1} ALREADY EXISTS on server {2}",
                        tenantDeployment.Application,
                        tenantDeployment.TenantId,
                        RoleEnvironment.CurrentRoleInstance.Id));

                tenantDeployment.Status = TenantDeploymentStatus.AppDeployed;

                return true;
            }

            // 1. extract tenant.zip can copy to IIS app folder
            var appPath = Path.Combine(this.sitesPath, tenantDeployment.HostName, DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture));
            this.appRepository.ExtractTenantApplication(tenantDeployment, appPath);

            // 2. add a webApp through IIS management API
            var newApp = new IISAppInfo
                {
                    ApplicationHostName = tenantDeployment.HostName,
                    CurrentPath = appPath,
                    Updated = (new FileInfo(appPath)).LastWriteTimeUtc
                };
            try
            {
                if (this.AddIISApp(newApp))
                {
                    this.installedApplications.Add(newApp);

                    tenantDeployment.Status = TenantDeploymentStatus.AppDeployed;
                    tenantDeployment.AppHostInstanceIp = Helper.HostIpAddress;
                    tenantDeployment.AppHostInstanceId = RoleEnvironment.CurrentRoleInstance.Id;
                    TenantDeploymentsStore.UpdateDeploymentStore(tenantDeployment);

                    return true;
                }
            }
            catch (Exception exception)
            {
                // remove this entry from both in-memory appList and tenant table
                if (this.installedApplications.Any(app => app.ApplicationHostName.Equals(newApp.ApplicationHostName)))
                {
                    this.installedApplications.Remove(newApp);
                }

                BowlusLogger.LogError(
                    string.Format(
                        "Error deploying the application {0} for tenant {1} on server {2}. Details are: {3}",
                        tenantDeployment.Application,
                        tenantDeployment.TenantId,
                        RoleEnvironment.CurrentRoleInstance.Id,
                        exception));
            }

            tenantDeployment.Status = TenantDeploymentStatus.DeploymentFailed;
            return false;
        }

        private bool DeleteApplication(TenantDeployment tenantDeployment)
        {
            var removed = false;

            // 1. remove a webApp through IIS management API
            // 2. delete app folder
            var serverManager = new ServerManager();
            var application = (from installedApp in this.installedApplications
                                where installedApp.ApplicationHostName.Equals(tenantDeployment.HostName)
                                select installedApp).FirstOrDefault();

            if (null != application)
            {
                RemoveIISApp(application);
                removed = true;
                tenantDeployment.Status = TenantDeploymentStatus.Undeployed;
            }

            return removed;
        }

        /// <summary>
        /// Configure the directory security.
        /// </summary>
        /// <param name="path">
        /// The path.
        /// </param>
        private void ConfigureDirectorySecurity(string path)
        {
            var sec = Directory.GetAccessControl(this.sitesPath);
            sec.AddAccessRule(new FileSystemAccessRule("Everyone", FileSystemRights.FullControl, InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit, PropagationFlags.None, AccessControlType.Allow));
            Directory.SetAccessControl(path, sec);
        }
    }
}
